1. Import test data¶
In [ ]:
import pandas as pd
import pandas_ta as ta
from statsmodels.nonparametric.kernel_regression import KernelReg
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from datetime import datetime
import matplotlib.pyplot as plt
from backtesting import Strategy
from backtesting import Backtest
import seaborn as sns
2. Load in the data¶
In [ ]:
df = pd.read_csv("EURUSD_Candlestick_15_M_BID_01.02.2023-17.02.2024.csv") # read in the data
df["Gmt time"]=df["Gmt time"].str.replace(".000","", regex=False) # remove the milliseconds; mention that we are not using regex but a simple string replace
df['Gmt time']=pd.to_datetime(df['Gmt time'],format='%d.%m.%Y %H:%M:%S') # convert the time string to datetime object
df=df[df.High!=df.Low] # remove rows where High=Low
3. Define the EMAs and ATR¶
In [ ]:
df["EMA_slow"]=ta.ema(df.Close, length=50)
df["EMA_fast"]=ta.ema(df.Close, length=40)
df['ATR']=ta.atr(df.High, df.Low, df.Close, length=7)
df
Out[ ]:
| Gmt time | Open | High | Low | Close | Volume | EMA_slow | EMA_fast | ATR | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 2023-02-01 00:00:00 | 1.08605 | 1.08619 | 1.08583 | 1.08604 | 2184.41 | NaN | NaN | NaN |
| 1 | 2023-02-01 00:15:00 | 1.08604 | 1.08623 | 1.08583 | 1.08609 | 2373.76 | NaN | NaN | NaN |
| 2 | 2023-02-01 00:30:00 | 1.08608 | 1.08637 | 1.08604 | 1.08630 | 1649.21 | NaN | NaN | NaN |
| 3 | 2023-02-01 00:45:00 | 1.08629 | 1.08638 | 1.08606 | 1.08622 | 2071.47 | NaN | NaN | NaN |
| 4 | 2023-02-01 01:00:00 | 1.08622 | 1.08634 | 1.08594 | 1.08607 | 2021.66 | NaN | NaN | NaN |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 18190 | 2024-02-16 20:45:00 | 1.07744 | 1.07770 | 1.07729 | 1.07764 | 3288.91 | 1.076952 | 1.077122 | 0.000443 |
| 18191 | 2024-02-16 21:00:00 | 1.07764 | 1.07771 | 1.07747 | 1.07763 | 1677.19 | 1.076979 | 1.077147 | 0.000414 |
| 18192 | 2024-02-16 21:15:00 | 1.07763 | 1.07778 | 1.07758 | 1.07776 | 964.40 | 1.077010 | 1.077176 | 0.000384 |
| 18193 | 2024-02-16 21:30:00 | 1.07777 | 1.07778 | 1.07761 | 1.07772 | 1957.77 | 1.077038 | 1.077203 | 0.000353 |
| 18194 | 2024-02-16 21:45:00 | 1.07773 | 1.07775 | 1.07747 | 1.07749 | 1693.33 | 1.077055 | 1.077217 | 0.000343 |
18176 rows × 9 columns
4. Define Nadaraya Watson Strategy¶
In [ ]:
dfsample = df[0:] # create a shadow coppy of the df. For simplicity, we can use part of the data.
X = dfsample.index # extract the index (time) of df and store it as variable X
# initialise the kernel regression model for nadaraya-watson kernel regression
# endog is the dependent variable, exog is the independent variable, var_type is the type of the variables, reg_type is the type of regression, bw is the bandwidth
# - Endogenous (dependent) variable: Close price
# - Exogenous (independent) variable: Time
# - Variable type: 'c' for continuous
# - Regression type: 'lc' for local constant another option is 'll' for local linear
# - Bandwidth: 3; the bandwidth is the window size of the kernel, a larger bandwidth will smooth the curve more a smaller bandwidth will make the curve more wiggly following the current data more closely
model = KernelReg(endog=dfsample['Close'], exog=dfsample.index, var_type='c', reg_type='lc', bw=[3])
# - fittd_values is the predicted values of the Close price
# - marginal_effects is the marginal effects in the Close price. I.e how a small change in the time will affect the Close price
fitted_values, marginal_effects = model.fit() # fit the model
# Add fitted values to DataFrame
dfsample['NW_Fitted'] = fitted_values
# Calculate residuals
# difference between the actual Close price and the predicted Close price; useful in understanding how well the model fits the data
residuals = dfsample['Close'] - fitted_values
# Calculate standard deviation of residuals
# standard deviation of the residuals is a measure of the spread of the residuals
# it is used to calculate the upper and lower envelopes
std_dev = 2.*np.std(residuals)
#std_dev = dfsample['Close'].rolling(window=30).std()
# Calculate upper and lower envelopes
dfsample['Upper_Envelope'] = dfsample['NW_Fitted'] + std_dev
dfsample['Lower_Envelope'] = dfsample['NW_Fitted'] - std_dev
dfsample[0:100]
/tmp/ipykernel_11488/2884411395.py:18: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame. Try using .loc[row_indexer,col_indexer] = value instead See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy dfsample['NW_Fitted'] = fitted_values /tmp/ipykernel_11488/2884411395.py:31: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame. Try using .loc[row_indexer,col_indexer] = value instead See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy dfsample['Upper_Envelope'] = dfsample['NW_Fitted'] + std_dev /tmp/ipykernel_11488/2884411395.py:32: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame. Try using .loc[row_indexer,col_indexer] = value instead See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy dfsample['Lower_Envelope'] = dfsample['NW_Fitted'] - std_dev
Out[ ]:
| Gmt time | Open | High | Low | Close | Volume | EMA_slow | EMA_fast | ATR | NW_Fitted | Upper_Envelope | Lower_Envelope | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2023-02-01 00:00:00 | 1.08605 | 1.08619 | 1.08583 | 1.08604 | 2184.41 | NaN | NaN | NaN | 1.086118 | 1.087100 | 1.085137 |
| 1 | 2023-02-01 00:15:00 | 1.08604 | 1.08623 | 1.08583 | 1.08609 | 2373.76 | NaN | NaN | NaN | 1.086109 | 1.087091 | 1.085127 |
| 2 | 2023-02-01 00:30:00 | 1.08608 | 1.08637 | 1.08604 | 1.08630 | 1649.21 | NaN | NaN | NaN | 1.086089 | 1.087071 | 1.085107 |
| 3 | 2023-02-01 00:45:00 | 1.08629 | 1.08638 | 1.08606 | 1.08622 | 2071.47 | NaN | NaN | NaN | 1.086055 | 1.087037 | 1.085073 |
| 4 | 2023-02-01 01:00:00 | 1.08622 | 1.08634 | 1.08594 | 1.08607 | 2021.66 | NaN | NaN | NaN | 1.086005 | 1.086987 | 1.085023 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 95 | 2023-02-01 23:45:00 | 1.10115 | 1.10129 | 1.10091 | 1.10124 | 1534.00 | 1.094777 | 1.095766 | 0.001213 | 1.101290 | 1.102272 | 1.100308 |
| 96 | 2023-02-02 00:00:00 | 1.10123 | 1.10182 | 1.10102 | 1.10151 | 4177.33 | 1.095041 | 1.096046 | 0.001154 | 1.101447 | 1.102428 | 1.100465 |
| 97 | 2023-02-02 00:15:00 | 1.10151 | 1.10218 | 1.10126 | 1.10192 | 2184.37 | 1.095311 | 1.096333 | 0.001120 | 1.101539 | 1.102521 | 1.100557 |
| 98 | 2023-02-02 00:30:00 | 1.10191 | 1.10237 | 1.10152 | 1.10220 | 4100.23 | 1.095581 | 1.096619 | 0.001082 | 1.101579 | 1.102561 | 1.100597 |
| 99 | 2023-02-02 00:45:00 | 1.10223 | 1.10266 | 1.10185 | 1.10185 | 4133.15 | 1.095827 | 1.096874 | 0.001043 | 1.101579 | 1.102561 | 1.100597 |
100 rows × 12 columns
5. Define the Bolinger Bands Calculation¶
In [ ]:
my_bbands = ta.bbands(dfsample.Close, length=30, std=2) # calculate the bollinger bands for the closing prices; lenght 30 means it looks at last 30 bars, std=2 means it uses 2 standard deviations
dfsample=dfsample.join(my_bbands)
#1.105424 1.097329
In [ ]:
dfsample.columns
Out[ ]:
Index(['Gmt time', 'Open', 'High', 'Low', 'Close', 'Volume', 'EMA_slow',
'EMA_fast', 'ATR', 'NW_Fitted', 'Upper_Envelope', 'Lower_Envelope',
'BBL_30_2.0', 'BBM_30_2.0', 'BBU_30_2.0', 'BBB_30_2.0', 'BBP_30_2.0'],
dtype='object')
6. Nadaraya Watson Envelopes Visualization¶
In [ ]:
# Create a plot with 1 row
fig = make_subplots(rows=1, cols=1)
# Add candlestick plot with customized line colors
fig.add_trace(go.Candlestick(x=dfsample.index,
open=dfsample['Open'],
high=dfsample['High'],
low=dfsample['Low'],
close=dfsample['Close'],
increasing=dict(line=dict(color='rgba(0, 255, 0, 0.6)', width=0.1), # Red with transparency for increasing
fillcolor='rgba(0, 255, 0, 0.6)'), # Match fill color with line color
decreasing=dict(line=dict(color='rgba(255, 0, 0, 0.6)', width=0.1), # Green with transparency for decreasing
fillcolor='rgba(255, 0, 0, 0.6)')), # Match fill color with line color
row=1, col=1)
# Add Nadaraya-Watson fitted line
fig.add_trace(go.Scatter(x=dfsample.index, y=dfsample['NW_Fitted'],
line=dict(color='green', width=2),
name="Nadaraya-Watson Fit"),
row=1, col=1)
# Add upper standard deviation envelope
fig.add_trace(go.Scatter(x=dfsample.index, y=dfsample['Upper_Envelope'],
line=dict(color='rgba(0,0,255,0.2)'), # Light blue color
name='Upper Envelope',
showlegend=False),
row=1, col=1)
# Add lower standard deviation envelope
fig.add_trace(go.Scatter(x=dfsample.index, y=dfsample['Lower_Envelope'],
line=dict(color='rgba(0,0,255,0.2)'), # Light blue color
name='Lower Envelope',
fill='tonexty', # This fills the area between this trace and the next trace
fillcolor='rgba(0,0,255,0.3)', # More opaque blue fill
showlegend=False),
row=1, col=1)
# # Add Bollinger Bands
# fig.add_trace(go.Scatter(x=dfsample.index, y=dfsample['BBU_30_2.0'],
# line=dict(color='rgba(0, 0, 255, 0.4)'), # Upper Band
# name='Upper Bollinger Band',
# showlegend=True),
# row=1, col=1)
# fig.add_trace(go.Scatter(x=dfsample.index, y=dfsample['BBM_30_2.0'],
# line=dict(color='rgba(0, 0, 255, 0.6)'), # Middle Band
# name='Middle Bollinger Band',
# showlegend=True),
# row=1, col=1)
# fig.add_trace(go.Scatter(x=dfsample.index, y=dfsample['BBL_30_2.0'],
# line=dict(color='rgba(0, 0, 255, 0.4)'), # Lower Band
# name='Lower Bollinger Band',
# fill='tonexty', # Fill between this line and the next upper band line
# fillcolor='rgba(0, 0, 255, 0.1)', # Light blue fill between the bands
# showlegend=True),
# row=1, col=1)
# Update layout to set background color to black and remove gridlines
fig.update_layout(
width=1500,
height=1200,
sliders=[],
paper_bgcolor='black', # Set the background color of the entire figure
plot_bgcolor='black', # Set the background color of the plotting area
xaxis_showgrid=False, # Remove x-axis gridlines
yaxis_showgrid=False, # Remove y-axis gridlines
)
# Show the plot
fig.show()
7. Signals based on EMA¶
In [ ]:
def ema_signal(df, backcandles):
# Create boolean Series for conditions
above = df['EMA_fast'] > df['EMA_slow'] # is the fast EMA above the slow EMA (bullish)
below = df['EMA_fast'] < df['EMA_slow'] # is the fast EMA below the slow EMA (bearish)
# Rolling window to check if condition is met consistently over the window
above_all = above.rolling(window=backcandles).apply(lambda x: x.all(), raw=True).fillna(0).astype(bool)
below_all = below.rolling(window=backcandles).apply(lambda x: x.all(), raw=True).fillna(0).astype(bool)
# Assign signals based on conditions (if the fast EMA is above or below the slow EMA)
df['EMASignal'] = 0 # Default no signal
df.loc[above_all, 'EMASignal'] = 2 # Signal 2 where EMA_fast consistently above EMA_slow (buy signal)
df.loc[below_all, 'EMASignal'] = 1 # Signal 1 where EMA_fast consistently below EMA_slow (sell signal)
return df # return the dataframe which at this point will contain the signal.
df = df[-60000:] # limit the dataframe to last 60k entries.
df.reset_index(inplace=True, drop=True) # because we selected a subset of the data, we need to reset the index
# TODO: check if dfsample is the correct dataframe to use.
df = ema_signal(dfsample, 7) # get signals from the EMA strategy using 7 backcandles (7x5minute = 35 minutes)
In [ ]:
df
Out[ ]:
| Gmt time | Open | High | Low | Close | Volume | EMA_slow | EMA_fast | ATR | NW_Fitted | Upper_Envelope | Lower_Envelope | BBL_30_2.0 | BBM_30_2.0 | BBU_30_2.0 | BBB_30_2.0 | BBP_30_2.0 | EMASignal | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2023-02-01 00:00:00 | 1.08605 | 1.08619 | 1.08583 | 1.08604 | 2184.41 | NaN | NaN | NaN | 1.086118 | 1.087100 | 1.085137 | NaN | NaN | NaN | NaN | NaN | 0 |
| 1 | 2023-02-01 00:15:00 | 1.08604 | 1.08623 | 1.08583 | 1.08609 | 2373.76 | NaN | NaN | NaN | 1.086109 | 1.087091 | 1.085127 | NaN | NaN | NaN | NaN | NaN | 0 |
| 2 | 2023-02-01 00:30:00 | 1.08608 | 1.08637 | 1.08604 | 1.08630 | 1649.21 | NaN | NaN | NaN | 1.086089 | 1.087071 | 1.085107 | NaN | NaN | NaN | NaN | NaN | 0 |
| 3 | 2023-02-01 00:45:00 | 1.08629 | 1.08638 | 1.08606 | 1.08622 | 2071.47 | NaN | NaN | NaN | 1.086055 | 1.087037 | 1.085073 | NaN | NaN | NaN | NaN | NaN | 0 |
| 4 | 2023-02-01 01:00:00 | 1.08622 | 1.08634 | 1.08594 | 1.08607 | 2021.66 | NaN | NaN | NaN | 1.086005 | 1.086987 | 1.085023 | NaN | NaN | NaN | NaN | NaN | 0 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 18190 | 2024-02-16 20:45:00 | 1.07744 | 1.07770 | 1.07729 | 1.07764 | 3288.91 | 1.076952 | 1.077122 | 0.000443 | 1.077652 | 1.078634 | 1.076670 | 1.074489 | 1.076929 | 1.079370 | 0.453217 | 0.645604 | 2 |
| 18191 | 2024-02-16 21:00:00 | 1.07764 | 1.07771 | 1.07747 | 1.07763 | 1677.19 | 1.076979 | 1.077147 | 0.000414 | 1.077638 | 1.078620 | 1.076657 | 1.074836 | 1.077047 | 1.079259 | 0.410649 | 0.631739 | 2 |
| 18192 | 2024-02-16 21:15:00 | 1.07763 | 1.07778 | 1.07758 | 1.07776 | 964.40 | 1.077010 | 1.077176 | 0.000384 | 1.077634 | 1.078616 | 1.076652 | 1.075116 | 1.077151 | 1.079187 | 0.377894 | 0.649531 | 2 |
| 18193 | 2024-02-16 21:30:00 | 1.07777 | 1.07778 | 1.07761 | 1.07772 | 1957.77 | 1.077038 | 1.077203 | 0.000353 | 1.077632 | 1.078614 | 1.076651 | 1.075506 | 1.077264 | 1.079021 | 0.326276 | 0.629830 | 2 |
| 18194 | 2024-02-16 21:45:00 | 1.07773 | 1.07775 | 1.07747 | 1.07749 | 1693.33 | 1.077055 | 1.077217 | 0.000343 | 1.077631 | 1.078613 | 1.076649 | 1.075802 | 1.077347 | 1.078891 | 0.286670 | 0.546410 | 2 |
18176 rows × 18 columns
8. Calculate total signal for the Nadaraya Watson envelope.¶
In [ ]:
def total_signal(df):
# Vectorized conditions for total_signal (buy when EMA signal is 2 and close price is below lower envelope, sell when EMA signal is 1 and close price is above upper envelope)
condition_buy = (df['EMASignal'] == 2) & (df['Close'] <= df['Lower_Envelope'])
condition_sell = (df['EMASignal'] == 1) & (df['Close'] >= df['Upper_Envelope'])
# Assigning signals based on conditions
df['Total_Signal'] = 0 # Default no signal
df.loc[condition_buy, 'Total_Signal'] = 2
df.loc[condition_sell, 'Total_Signal'] = 1
total_signal(dfsample)
In [ ]:
# Count the signals
dfsample["TotalSignal"]=dfsample.Total_Signal
dfsample.TotalSignal.value_counts()
Out[ ]:
0 17849 2 183 1 144 Name: TotalSignal, dtype: int64
Point of the total signal which takes under the consideration EMA and Nadaraya Watson envelope.¶
In [ ]:
def pointpos(x):
if x['TotalSignal']==2:
return x['Low']-1e-4
elif x['TotalSignal']==1:
return x['High']+1e-4
else:
return np.nan
df['pointpos'] = df.apply(lambda row: pointpos(row), axis=1)
Visualise the total signal including the EMA and Nadaraya Watson envelope.¶
In [ ]:
dfpl = df
# Create a plot with 2 rows
fig = make_subplots(rows=2, cols=1)
# Add candlestick plot on the first row
fig.add_trace(go.Candlestick(x=dfpl.index,
open=dfpl['Open'],
high=dfpl['High'],
low=dfpl['Low'],
close=dfpl['Close']),
row=1, col=1)
# # # Add Bollinger Bands, EMA lines on the same subplot
# fig.add_trace(go.Scatter(x=dfpl.index, y=dfpl['BBL_30_2.0'],
# line=dict(color='green', width=1),
# name="BBL"),
# row=1, col=1)
# fig.add_trace(go.Scatter(x=dfpl.index, y=dfpl['BBU_30_2.0'],
# line=dict(color='green', width=1),
# name="BBU"),
# row=1, col=1)
# fig.add_trace(go.Scatter(x=dfpl.index, y=dfpl['EMA_fast'],
# line=dict(color='black', width=1),
# name="EMA_fast"),
# row=1, col=1)
# fig.add_trace(go.Scatter(x=dfpl.index, y=dfpl['EMA_slow'],
# line=dict(color='blue', width=1),
# name="EMA_slow"),
# row=1, col=1)
fig.add_trace(go.Scatter(x=dfpl.index, y=dfpl['Upper_Envelope'], line=dict(color='red', width=1), name="Upper Envelope"), row=1, col=1)
fig.add_trace(go.Scatter(x=dfpl.index, y=dfpl['Lower_Envelope'], line=dict(color='green', width=1), name="Lower Envelope"), row=1, col=1)
# Add markers for trade entry points on the same subplot
fig.add_trace(go.Scatter(x=dfpl.index, y=dfpl['pointpos'], mode="markers",
marker=dict(size=4, color="MediumPurple"),
name="entry"),
row=1, col=1)
fig.update_layout(
width=1500,
height=1200,
sliders=[],
paper_bgcolor='black', # Set the background color of the entire figure
plot_bgcolor='black', # Set the background color of the plotting area
xaxis_showgrid=False, # Remove x-axis gridlines
yaxis_showgrid=False, # Remove y-axis gridlines
)
fig.show()
9. Back test the Nadaraya Watson envelope strategy.¶
In [ ]:
dfopt = dfsample[0:] # copy of the dataframe
def SIGNAL():
return dfopt.TotalSignal # 0 none 1 sell 2 buy
class MyStrat(Strategy):
mysize = 3000 # size of trades
slcoef = 1.1 # stop loss coeficient
TPSLRatio = 1.5 # take profit and stop loss ratio
def init(self): # sets strategy variables
super().init()
self.signal1 = self.I(SIGNAL)
def next(self): # defines the trading strategy
super().next()
slatr = self.slcoef*self.data.ATR[-1] # stop loss adjusted to volatility
TPSLRatio = self.TPSLRatio # take profit
if self.signal1==1 and len(self.trades)==0:
sl1 = self.data.Close[-1] - slatr
tp1 = self.data.Close[-1] + slatr*TPSLRatio
self.buy(sl=sl1, tp=tp1, size=self.mysize)
elif self.signal1==2 and len(self.trades)==0:
sl1 = self.data.Close[-1] + slatr
tp1 = self.data.Close[-1] - slatr*TPSLRatio
self.sell(sl=sl1, tp=tp1, size=self.mysize)
bt = Backtest(dfopt, MyStrat, cash=250, margin=1/30)
stats, heatmap = bt.optimize(slcoef=[i/10 for i in range(10, 26)],
TPSLRatio=[i/10 for i in range(10, 26)],
maximize='Return [%]', max_tries=300,
random_state=0,
return_heatmap=True)
stats
/tmp/ipykernel_11488/79916414.py:29: UserWarning: Data index is not datetime. Assuming simple periods, but `pd.DateTimeIndex` is advised.
Backtest.optimize: 0%| | 0/8 [00:00<?, ?it/s]
Out[ ]:
Start 0.0 End 18194.0 Duration 18194.0 Exposure Time [%] 9.804137 Equity Final [$] 105.122141 Equity Peak [$] 260.391569 Return [%] -57.951144 Buy & Hold Return [%] -0.787264 Return (Ann.) [%] 0.0 Volatility (Ann.) [%] NaN Sharpe Ratio NaN Sortino Ratio NaN Calmar Ratio 0.0 Max. Drawdown [%] -61.24997 Avg. Drawdown [%] -15.773511 Max. Drawdown Duration 16788.0 Avg. Drawdown Duration 2988.166667 # Trades 67.0 Win Rate [%] 17.910448 Best Trade [%] 1.105764 Worst Trade [%] -0.508768 Avg. Trade [%] -0.067063 Max. Trade Duration 278.0 Avg. Trade Duration 25.656716 Profit Factor 0.638838 Expectancy [%] -0.066407 SQN -1.50331 _strategy MyStrat(slcoef=2... _equity_curve Equit... _trades Size EntryB... dtype: object
In [ ]:
import plotly.graph_objects as go
from plotly.subplots import make_subplots
def plot_trades_plotly(df, strategy):
"""Function to plot trades, including entries, exits, SL, TP using Plotly."""
entries = strategy.entries
exits = strategy.exits
sl_levels = strategy.sl_levels
tp_levels = strategy.tp_levels
trade_types = strategy.trade_types
# Create a plot with 1 row (main candlestick chart)
fig = make_subplots(rows=1, cols=1, shared_xaxes=True)
# Add candlestick plot with customized line colors
fig.add_trace(go.Candlestick(
x=df.index,
open=df['Open'],
high=df['High'],
low=df['Low'],
close=df['Close'],
increasing=dict(line=dict(color='rgba(0, 255, 0, 0.6)', width=1), fillcolor='rgba(0, 255, 0, 0.6)'),
decreasing=dict(line=dict(color='rgba(255, 0, 0, 0.6)', width=1), fillcolor='rgba(255, 0, 0, 0.6)')
))
# Plot entry points
for i, (entry_time, entry_price, trade_type) in enumerate(entries):
color = 'green' if trade_type == 'long' else 'red'
symbol = 'triangle-up' if trade_type == 'long' else 'triangle-down'
fig.add_trace(go.Scatter(
x=[entry_time], y=[entry_price],
mode='markers',
marker=dict(symbol=symbol, color=color, size=10),
name=f'Entry ({trade_type.capitalize()})',
hovertemplate=f'Entry ({trade_type.capitalize()}): %{y:.2f}<extra></extra>'
))
# Add stop loss (SL) and take profit (TP) lines
fig.add_trace(go.Scatter(
x=[entry_time, entry_time],
y=[sl_levels[i], tp_levels[i]],
mode='lines',
line=dict(dash='dash', color='rgba(255, 0, 0, 0.5)' if trade_type == 'long' else 'rgba(0, 255, 0, 0.5)'),
hoverinfo='skip',
showlegend=False
))
# Plot exit points
for i, (exit_time, exit_price) in enumerate(exits):
fig.add_trace(go.Scatter(
x=[exit_time], y=[exit_price],
mode='markers',
marker=dict(symbol='circle', color='orange', size=10),
name=f'Exit',
hovertemplate=f'Exit: %{y:.2f}<extra></extra>'
))
# Add Nadaraya-Watson fitted line
fig.add_trace(go.Scatter(
x=df.index, y=df['NW_Fitted'],
line=dict(color='green', width=2),
name="Nadaraya-Watson Fit"
))
# Add upper and lower envelopes (standard deviation bands)
fig.add_trace(go.Scatter(
x=df.index, y=df['Upper_Envelope'],
line=dict(color='rgba(0,0,255,0.2)'), # Light blue color for upper envelope
name='Upper Envelope',
showlegend=False
))
fig.add_trace(go.Scatter(
x=df.index, y=df['Lower_Envelope'],
line=dict(color='rgba(0,0,255,0.2)'), # Light blue color for lower envelope
name='Lower Envelope',
fill='tonexty', # Fills the area between the lower envelope and the upper envelope
fillcolor='rgba(0,0,255,0.3)', # More opaque blue fill
showlegend=False
))
# Customize layout: set background color, gridlines, and make the chart zoom-friendly
fig.update_layout(
width=1500,
height=800,
sliders=[],
paper_bgcolor='black', # Background color of the entire figure
plot_bgcolor='black', # Background color of the plotting area
xaxis_showgrid=False, # Remove x-axis gridlines
yaxis_showgrid=False, # Remove y-axis gridlines
xaxis_rangeslider_visible=False, # Hide the range slider for cleaner zoom
xaxis=dict(
rangeselector=dict(
buttons=list([
dict(count=1, label="1m", step="month", stepmode="backward"),
dict(count=6, label="6m", step="month", stepmode="backward"),
dict(step="all")
])
),
type="category", # Ensure better zooming when there are missing or irregular time intervals
rangebreaks=[dict(bounds=["sat", "mon"])], # Breaks for weekends, where no trading occurs
rangeslider=dict(visible=True, thickness=0.1) # Adds a thinner range slider for zoom
),
)
# Update y-axis for better zoom interaction
fig.update_yaxes(fixedrange=False)
# Show the plot
fig.show()
# Example usage after running backtest
bt = Backtest(dfopt, MyStrat, cash=250, margin=1/30)
stats = bt.run()
# Get strategy instance and plot trades using the enhanced function
strategy_instance = bt._strategy
plot_trades_plotly(dfopt, strategy_instance)
/tmp/ipykernel_11488/2219177076.py:114: UserWarning: Data index is not datetime. Assuming simple periods, but `pd.DateTimeIndex` is advised.
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) Cell In[27], line 119 117 # Get strategy instance and plot trades using the enhanced function 118 strategy_instance = bt._strategy --> 119 plot_trades_plotly(dfopt, strategy_instance) Cell In[27], line 7, in plot_trades_plotly(df, strategy) 4 def plot_trades_plotly(df, strategy): 5 """Function to plot trades, including entries, exits, SL, TP using Plotly.""" ----> 7 entries = strategy.entries 8 exits = strategy.exits 9 sl_levels = strategy.sl_levels AttributeError: type object 'MyStrat' has no attribute 'entries'
In [ ]:
stats["_strategy"]
Out[ ]:
<Strategy MyStrat(slcoef=2.0,TPSLRatio=2.1)>
In [ ]:
# Convert multiindex series to dataframe
heatmap_df = heatmap.unstack()
plt.figure(figsize=(10, 8))
sns.heatmap(heatmap_df, annot=True, cmap='viridis', fmt='.0f')
plt.show()
In [ ]:
dftest = df[:]
def SIGNAL():
return dftest.TotalSignal
class MyStrat(Strategy):
mysize = 3000
slcoef = 2.6
TPSLRatio = 2.6
def init(self):
super().init()
self.signal1 = self.I(SIGNAL)
def next(self):
super().next()
slatr = self.slcoef*self.data.ATR[-1]
TPSLRatio = self.TPSLRatio
if self.signal1==1 and len(self.trades)==0:
sl1 = self.data.Close[-1] - slatr
tp1 = self.data.Close[-1] + slatr*TPSLRatio
self.buy(sl=sl1, tp=tp1, size=self.mysize)
elif self.signal1==2 and len(self.trades)==0:
sl1 = self.data.Close[-1] + slatr
tp1 = self.data.Close[-1] - slatr*TPSLRatio
self.sell(sl=sl1, tp=tp1, size=self.mysize)
bt = Backtest(dftest, MyStrat, cash=250, margin=1/30, commission=0.0002)
bt.run()
/tmp/ipykernel_11488/1347927932.py:29: UserWarning: Data index is not datetime. Assuming simple periods, but `pd.DateTimeIndex` is advised.
Out[ ]:
Start 0.0 End 18194.0 Duration 18194.0 Exposure Time [%] 18.777509 Equity Final [$] 92.970842 Equity Peak [$] 264.467407 Return [%] -62.811663 Buy & Hold Return [%] -0.787264 Return (Ann.) [%] 0.0 Volatility (Ann.) [%] NaN Sharpe Ratio NaN Sortino Ratio NaN Calmar Ratio 0.0 Max. Drawdown [%] -64.846012 Avg. Drawdown [%] -11.245286 Max. Drawdown Duration 16272.0 Avg. Drawdown Duration 1792.2 # Trades 60.0 Win Rate [%] 15.0 Best Trade [%] 1.507011 Worst Trade [%] -0.679918 Avg. Trade [%] -0.081148 Max. Trade Duration 764.0 Avg. Trade Duration 56.016667 Profit Factor 0.650433 Expectancy [%] -0.079987 SQN -1.287177 _strategy MyStrat _equity_curve Equit... _trades Size EntryB... dtype: object
In [ ]:
bt.plot()
Loading "original-fs" failed Error: Cannot find module 'original-fs' Require stack: - /root/.vscode-server/bin/fee1edb8d6d72a0ddff41e5f71a671c23ed924b9/out/server-cli.js at Module._resolveFilename (node:internal/modules/cjs/loader:1145:15) at Module._load (node:internal/modules/cjs/loader:986:27) at Module.require (node:internal/modules/cjs/loader:1233:19) at require (node:internal/modules/helpers:179:18) at i (/root/.vscode-server/bin/fee1edb8d6d72a0ddff41e5f71a671c23ed924b9/out/server-cli.js:3:98) at r.load (/root/.vscode-server/bin/fee1edb8d6d72a0ddff41e5f71a671c23ed924b9/out/server-cli.js:2:1637) at h.load (/root/.vscode-server/bin/fee1edb8d6d72a0ddff41e5f71a671c23ed924b9/out/server-cli.js:1:13958) at u (/root/.vscode-server/bin/fee1edb8d6d72a0ddff41e5f71a671c23ed924b9/out/server-cli.js:3:9338) at Object.errorback (/root/.vscode-server/bin/fee1edb8d6d72a0ddff41e5f71a671c23ed924b9/out/server-cli.js:3:9457) at h.triggerErrorback (/root/.vscode-server/bin/fee1edb8d6d72a0ddff41e5f71a671c23ed924b9/out/server-cli.js:1:14252) at /root/.vscode-server/bin/fee1edb8d6d72a0ddff41e5f71a671c23ed924b9/out/server-cli.js:1:14003 at r.load (/root/.vscode-server/bin/fee1edb8d6d72a0ddff41e5f71a671c23ed924b9/out/server-cli.js:2:1654) at h.load (/root/.vscode-server/bin/fee1edb8d6d72a0ddff41e5f71a671c23ed924b9/out/server-cli.js:1:13958) at u (/root/.vscode-server/bin/fee1edb8d6d72a0ddff41e5f71a671c23ed924b9/out/server-cli.js:3:9338) at l._loadModule (/root/.vscode-server/bin/fee1edb8d6d72a0ddff41e5f71a671c23ed924b9/out/server-cli.js:3:9466) at l._resolve (/root/.vscode-server/bin/fee1edb8d6d72a0ddff41e5f71a671c23ed924b9/out/server-cli.js:4:452) at l.defineModule (/root/.vscode-server/bin/fee1edb8d6d72a0ddff41e5f71a671c23ed924b9/out/server-cli.js:3:5561) at Function.p [as define] (/root/.vscode-server/bin/fee1edb8d6d72a0ddff41e5f71a671c23ed924b9/out/server-cli.js:4:1741) at out-build/bootstrap-amd.js (/root/.vscode-server/bin/fee1edb8d6d72a0ddff41e5f71a671c23ed924b9/out/server-cli.js:4:6445) at /root/.vscode-server/bin/fee1edb8d6d72a0ddff41e5f71a671c23ed924b9/out/server-cli.js:1:132 at Object.<anonymous> (/root/.vscode-server/bin/fee1edb8d6d72a0ddff41e5f71a671c23ed924b9/out/server-cli.js:4:9653) at Module._compile (node:internal/modules/cjs/loader:1358:14) at Module._extensions..js (node:internal/modules/cjs/loader:1416:10) at Module.load (node:internal/modules/cjs/loader:1208:32) at Module._load (node:internal/modules/cjs/loader:1024:12) at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:174:12) at node:internal/main/run_main_module:28:49 { code: 'MODULE_NOT_FOUND', requireStack: [ '/root/.vscode-server/bin/fee1edb8d6d72a0ddff41e5f71a671c23ed924b9/out/server-cli.js' ], phase: 'loading', moduleId: 'original-fs', neededBy: [ 'fs' ] } Here are the modules that depend on it: [ 'fs' ]
Out[ ]:
GridPlot(
id = 'p1343', …)
In [ ]:
def gaussian_kernel(x, bandwidth):
return (1 / (np.sqrt(2 * np.pi) * bandwidth)) * np.exp(-0.5 * ((x / bandwidth) ** 2))
def compute_weights(X, x, bandwidth):
weights = [gaussian_kernel(x - i, bandwidth) for i in X]
weights /= np.sum(weights) # Normalize to make the weights sum to 1
return weights
# Generate synthetic price data
np.random.seed(0)
prices = np.array([2 for i in range(0, 100)]) # 100 price points
# Select the current price point (most recent)
current_price_index = 99 # Last price in the series
current_price = prices[current_price_index]
# Bandwidth (standard deviation of the kernel)
bandwidth = 10
# Compute weights for all preceding points
weights = compute_weights(range(0, 99), current_price_index, bandwidth)
# Plot the weights distribution
plt.figure(figsize=(10, 5))
plt.bar(range(current_price_index), weights, color='blue')
plt.title('Weight Distribution for Nadaraya-Watson Estimator')
plt.xlabel('Index of Price Points')
plt.ylabel('Weight')
plt.grid(False)
plt.gca().set_facecolor('black') # Change background color of the plot to black
plt.show()
In [ ]: